import multiparty from "multiparty";
import fse from "fs-extra";
import { Result, errCodeAndMsg } from "@/utils/result";
import path from "path";
import { Context, Next } from "koa";
import { ChunkFile, ChunkFormData, FileFormData, Merge, UploadFile, Verify } from "@/controller/upload.router";
import { pipeStream } from "@/utils/file";

/**
 * 分片将会上传到根目录 /[UPLOAD_PATH]/chunk/[fileHash] 文件夹下临时存储，分片命名：[chunkHash].[ext]
 * eg：upload\chunks\0ef9c7de754a9418943f80ad9d99a907\0ef9c7de754a9418943f80ad9d99a907-1.jpg
 *
 * 分片合并后，完整的文件会放到 /[UPLOAD_PATH] 根目录下，并且命名为：[fileHash].[ext]
 * eg：upload\0ef9c7de754a9418943f80ad9d99a907.jpg
 */

// 上传后资源的URL地址
const RESOURCE_URL = `${process.env.SERVER_HOST}:${process.env.SERVER_PORT}/`;
// 上传文件的存储目录(/upload)
const UPLOAD_DIR = path.resolve(process.cwd(), `${process.env.UPLOAD_PATH}`);
// 分片存放的目录（/upload/chunks）
const CHUNK_DIR = path.resolve(UPLOAD_DIR, "./chunks/");

/**
 * 普通文件上传
 */
const uploadFile = async (ctx: Context, next: Next) => {
    const form = new multiparty.Form();
    try {
        const { fileHash, filename, file } = await new Promise<FileFormData>((resolve, rejects) => {
            form.parse(
                ctx.req,
                async (
                    err,
                    fields: Record<keyof FileFormData, any[]>,
                    files: Record<keyof FileFormData, UploadFile[]>
                ) => {
                    if (err) rejects(err);

                    // 读取表单非文件字段数据
                    const [fileHash] = fields.fileHash;
                    const [filename] = fields.filename;
                    // 读取表单文件字段数据
                    const [file] = files.file;

                    resolve({ fileHash, filename, file });
                }
            );
        });

        const ext = filename.split(".").pop() ?? "";
        const filePath = path.resolve(UPLOAD_DIR, `${fileHash}.${ext}`);
        const accessUrl = new URL(path.join(RESOURCE_URL, `${fileHash}.${ext}`));

        // 1. 判断文件是否已经存在，存在就直接返回
        if (fse.existsSync(filePath)) return Result.ok(ctx, { filename, url: accessUrl }, "文件已存在");
        try {
            // 2. 将文件从临时文件夹移动输出到存放目录
            await fse.move(file.path, filePath);
            return Result.ok(ctx, { filename, url: accessUrl }, "文件上传成功");
        } catch (error: any) {
            return Result.err(ctx, errCodeAndMsg.FILE_UPLOAD_FAIL);
        }
    } catch (error) {
        return Result.err(ctx, errCodeAndMsg.PARAM_NOT_VALID);
    }
};
/**
 * 上传分片
 */
const uploadChunk = async (ctx: Context, next: Next) => {
    // 获取 formdata 数据
    const form = new multiparty.Form();
    try {
        const {
            fileHash,
            chunkHash,
            chunkIndex,
            filename,
            chunk: chunkFile
        } = await new Promise<ChunkFormData>((resolve, rejects) => {
            form.parse(
                ctx.req,
                async (
                    err,
                    fields: Record<keyof ChunkFormData, any[]>,
                    files: Record<keyof ChunkFormData, ChunkFile[]>
                ) => {
                    if (err) rejects(err);

                    // 读取表单非文件字段数据
                    const [fileHash] = fields.fileHash;
                    const [chunkHash] = fields.chunkHash;
                    const [chunkIndex] = fields.chunkIndex;
                    const [filename] = fields.filename;
                    // 读取表单文件字段数据
                    const [chunk] = files.chunk;

                    resolve({ fileHash, chunkHash, chunkIndex, filename, chunk });
                }
            );
        });

        const ext = filename.split(".").pop() ?? "";
        const filePath = path.resolve(UPLOAD_DIR, `${fileHash}.${ext}`);
        const chunkDir = path.resolve(CHUNK_DIR, fileHash);
        const chunkPath = path.resolve(chunkDir, `${chunkHash}.${ext}`);

        // 1. 判断大文件是否已经存在，存在就直接返回
        if (fse.existsSync(filePath)) return Result.ok(ctx, { filename }, "文件已存在");

        try {
            // 2. 是否存在该文件的分片目录，没有就创建它
            fse.ensureDirSync(chunkDir);

            // 3. 判断该分片是否已经存在，已经存在则删除它
            if (fse.existsSync(chunkPath)) await fse.remove(chunkPath);

            // 4. 将分片放入分片文件夹（上传后文件会暂存在一个临时文件夹，而不是在内存里，所以只要移动分片即可）
            await fse.move(chunkFile.path, chunkPath);

            return Result.ok(ctx, { chunkHash, filename }, "分片上传成功");
        } catch (error: any) {
            return Result.err(ctx, errCodeAndMsg.FILE_UPLOAD_FAIL);
        }
    } catch (error) {
        return Result.err(ctx, errCodeAndMsg.PARAM_NOT_VALID);
    }
};

/**
 * 合并分片
 */
const mergeChunk = async (ctx: Context, next) => {
    const { fileHash, filename, chunkSize } = ctx.request.body as Merge;
    // 获取文件后缀
    const ext = filename.slice(filename.lastIndexOf(".") + 1);
    // 合并后的文件路径（路径包含文件名）
    const _filePath = path.resolve(UPLOAD_DIR, fileHash) + "." + ext;
    // 获取该文件的分片目录
    const _chunkDir = path.resolve(CHUNK_DIR, fileHash);

    const accessUrl = new URL(path.join(RESOURCE_URL, `${fileHash}.${ext}`));

    // 1. 判断分片文件是否已经存在，存在就直接返回
    if (fse.existsSync(_filePath)) {
        return Result.ok(
            ctx,
            {
                fileHash,
                filename,
                url: accessUrl
            },
            "文件已存在"
        );
    }
    // 获取分片文件夹内的分片文件名
    const _chunkNameList: string[] = await fse.readdir(_chunkDir);
    // 按切割时的分片索引排序
    _chunkNameList.sort((a, b) => +a.split("-")[1] - +b.split("-")[1]);

    try {
        // 并行读取切片，以流的形式将分片写入到文件中对应位置
        await Promise.all(
            _chunkNameList.map((chunkName, index) => {
                // 在大文件指定位置创建可写流
                const _chunkWriteStream = fse.createWriteStream(_filePath, {
                    start: index * chunkSize
                });
                // 分片写入大文件
                return pipeStream(path.resolve(_chunkDir, chunkName), _chunkWriteStream);
            })
        );
        // 删除分片目录
        fse.rmdirSync(_chunkDir);
    } catch (error) {
        console.log("error", error);
        return Result.err(ctx, { code: 1001, msg: "合并切片失败" });
    }
    return Result.ok(
        ctx,
        {
            fileHash,
            filename,
            url: accessUrl
        },
        "合并切片成功"
    );
};

/**
 * 获取文件访问链接
 */
const getAccessUrl = async (ctx, next) => {
    const { fileHash } = ctx.request.params;

    const fileList: string[] = await fse.readdir(UPLOAD_DIR);
    const filename = fileList.find(item => item.split(".")[0] === fileHash);

    if (!filename) return Result.err(ctx, { code: 1001, msg: "文件不存在" });

    const accessUrl = new URL(path.join(RESOURCE_URL, `${filename}`));

    return Result.ok(ctx, { url: accessUrl });
};

/**
 * 验证文件是否上传
 * 1. 已上传：返回访问链接
 * 2. 未上传：返回已经完全上传完毕的分片名数组，完全没上传分片数组就是空数组
 */
const verifyFileUpload = async (ctx, next) => {
    const { fileHash, chunkSize } = ctx.request.body as Verify;

    const fileList: string[] = await fse.readdir(UPLOAD_DIR);
    const filename = fileList.find(item => item.split(".")[0] === fileHash);
    const currentChunkDir = path.join(CHUNK_DIR, fileHash);

    let uploadedChunks: string[] = [];

    if (!filename) {
        // 大文件不存在
        if (fse.existsSync(currentChunkDir)) {
            // 分片文件夹存在
            const chunkList = await fse.readdir(currentChunkDir);
            // 过滤掉分块大小小于 chunkSize 的分块
            uploadedChunks = chunkList.filter(item => {
                const chunkStatus = fse.statSync(path.resolve(currentChunkDir, item));
                if (chunkStatus.size !== chunkSize) return false;
                return true;
            });
        }
        return Result.ok(ctx, { fileHash, uploadedChunks, url: "" });
    }

    const accessUrl = new URL(path.join(RESOURCE_URL, `${filename}`));
    return Result.ok(ctx, { fileHash, uploadedChunks, url: accessUrl });
};

export { uploadFile, uploadChunk, mergeChunk, getAccessUrl, verifyFileUpload };
